Completed
Push — master ( 8caa6f...c37a57 )
by Jan
20s queued 13s
created

Frame.ts ➔ handleMultipleAndMakeFrameBuffer   A

Complexity

Conditions 2

Size

Total Lines 15
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 13
dl 0
loc 15
rs 9.75
c 0
b 0
f 0
cc 2
1
import zlib = require('zlib')
2
import {
3
    Flags,
4
    getHeaderSize,
5
    FrameHeader
6
} from './FrameHeader'
7
import * as GenericFrames from './frames/generic'
8
import { Frames } from './frames/frames'
9
import { isKeyOf } from "./util"
10
11
type HeaderInfo = {
12
    identifier: string
13
    headerSize: number
14
    bodySize: number
15
    flags: Flags
16
}
17
18
export class Frame {
19
    identifier: string
20
    private value: unknown
21
    flags: Flags
22
23
    constructor(identifier: string, value: unknown, flags: Flags = {}) {
24
        this.identifier = identifier
25
        this.value = value
26
        this.flags = flags
27
    }
28
29
    static createFromBuffer = createFromBuffer
30
31
    getValue() {
32
        return this.value
33
    }
34
}
35
36
type FrameData = {
37
    header: HeaderInfo
38
    body: Buffer
39
}
40
41
function getFrameDataFromFrameBuffer(
42
    frameBuffer: Buffer,
43
    version: number
44
): FrameData | null {
45
    const headerSize = getHeaderSize(version)
46
    // Specification requirement
47
    if (frameBuffer.length < headerSize + 1) {
48
        return null
49
    }
50
    const headerBuffer = frameBuffer.subarray(0, headerSize)
51
    const header: HeaderInfo = {
52
        headerSize,
53
        ...FrameHeader.createFromBuffer(headerBuffer, version)
54
    }
55
    if (header.flags.encryption) {
56
        return null
57
    }
58
59
    const body = decompressBody(
60
        header.flags,
61
        getDataLength(header, frameBuffer),
62
        getBody(header, frameBuffer)
63
    )
64
    if (!body) {
65
        return null
66
    }
67
    return { header, body }
68
}
69
70
function createFromBuffer(
71
    frameBuffer: Buffer,
72
    version: number
73
): Frame | null {
74
    const frameData = getFrameDataFromFrameBuffer(frameBuffer, version)
75
    if (!frameData) {
76
        return null
77
    }
78
    const { header, body } = frameData
79
    const value = makeFrameValue(header.identifier, body, version)
80
    if (!value) {
81
        return null
82
    }
83
    return new Frame(header.identifier, value, header.flags)
84
}
85
86
function makeFrameValue(identifier:string, body: Buffer, version: number) {
87
    try {
88
        if (isKeyOf(identifier, Frames)) {
89
            return Frames[identifier].read(body, version)
90
        }
91
        if (identifier.startsWith('T')) {
92
            return GenericFrames.GENERIC_TEXT.read(body)
93
        }
94
        if (identifier.startsWith('W')) {
95
            return GenericFrames.GENERIC_URL.read(body)
96
        }
97
    } catch(error) {
98
        // On read ignore frames with errors
99
        return null
100
    }
101
    return null
102
}
103
104
function getBody({flags, headerSize, bodySize}: HeaderInfo, buffer: Buffer) {
105
    const bodyOffset = flags.dataLengthIndicator ? 4 : 0
106
    const bodyStart = headerSize + bodyOffset
107
    const bodyEnd = bodyStart + bodySize - bodyOffset
108
    const body = buffer.subarray(bodyStart, bodyEnd)
109
    if (flags.unsynchronisation) {
110
        // This method should stay in ID3Util for now because it's also used
111
        // in the Tag's header which we don't have a class for.
112
        return processUnsynchronisedBuffer(body)
113
    }
114
    return body
115
}
116
117
function processUnsynchronisedBuffer(buffer: Buffer) {
118
    const newDataArr = []
119
    if (buffer.length > 0) {
120
        newDataArr.push(buffer[0])
121
    }
122
    for(let i = 1; i < buffer.length; i++) {
123
        if (buffer[i - 1] === 0xFF && buffer[i] === 0x00) {
124
            continue
125
        }
126
        newDataArr.push(buffer[i])
127
    }
128
    return Buffer.from(newDataArr)
129
}
130
131
function getDataLength({flags, headerSize}: HeaderInfo, buffer: Buffer) {
132
    return flags.dataLengthIndicator ? buffer.readInt32BE(headerSize) : 0
133
}
134
135
function decompressBody(
136
    {compression}: Flags,
137
    dataLength: number,
138
    body: Buffer
139
) {
140
    return compression ? decompressBuffer(body, dataLength) : body
141
}
142
143
function decompressBuffer(buffer: Buffer, expectedDecompressedLength: number) {
144
    if (buffer.length < 5) {
145
        return null
146
    }
147
148
    // ID3 spec defines that compression is stored in ZLIB format,
149
    // but doesn't specify if header is present or not.
150
    // ZLIB has a 2-byte header.
151
    // 1. try if header + body decompression
152
    // 2. else try if header is not stored (assume that all content is deflated "body")
153
    // 3. else try if inflation works if the header is omitted (implementation dependent)
154
    const decompressed = (
155
        tryFunc(() => zlib.inflateSync(buffer)) ??
156
        tryFunc(() => zlib.inflateRawSync(buffer)) ??
157
        tryFunc(() => zlib.inflateRawSync(buffer.subarray(2)))
158
    )
159
    if (decompressed && decompressed.length === expectedDecompressedLength) {
160
        return decompressed
161
    }
162
    return null
163
}
164
165
const tryFunc = (func: () => Buffer) => {
166
    try {
167
        return func()
168
    } catch(error) {
169
        return null
170
    }
171
}
172